package net.stogi.dsper.back.service.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;

import net.stogi.common.api.dto.OrderBy;
import net.stogi.common.api.exception.BusinessException;
import net.stogi.dsper.api.dto.DataObjectDto;
import net.stogi.dsper.api.dto.DataSourceDto;
import net.stogi.dsper.api.dto.TagDto;
import net.stogi.dsper.api.dto.UserDto;
import net.stogi.dsper.api.dto.UserProfileDto;
import net.stogi.dsper.api.model.DataField;
import net.stogi.dsper.api.model.DataObject;
import net.stogi.dsper.api.model.DataSource;
import net.stogi.dsper.api.model.Tag;
import net.stogi.dsper.api.model.User;
import net.stogi.dsper.api.model.UserProfile;
import net.stogi.dsper.api.service.UserService;
import net.stogi.dsper.api.util.ModelToDtoConverterUtils;
import net.stogi.dsper.back.dao.FeedDao;
import net.stogi.dsper.back.dao.UserDao;
import net.stogi.dsper.back.dao.UserProfileDao;
import net.stogi.dsper.back.service.DataSourceInternalService;
import net.stogi.dsper.back.service.TagInternalService;
import net.stogi.dsper.back.service.UserInternalService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service("DsperBack.Service.UserService")
public class UserServiceImpl implements UserService, UserInternalService {

	private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);

	@Autowired
	private DataSourceInternalService dataSourceService;

	@Autowired
	private FeedDao feedDao;

	@Autowired
	private TagInternalService tagService;

	@Autowired
	private UserDao userDao;

	@Autowired
	private UserProfileDao userProfileDao;

	@Override
	@Transactional(rollbackFor = BusinessException.class)
	public UserDto getUserByUsername(String username) throws BusinessException {
		assert username != null : "Parameter 'username' can not be null";

		// Load user.
		User user = userDao.loadByUsername(username);

		// Check user.
		if (user == null) {
			log.warn("No user with given username: [username = '{}']", username);
			throw new BusinessException("No user with given username: [username = '" + username + "']");
		}

		return ModelToDtoConverterUtils.convertUserModelToDto(user);
	}

	@Override
	public User getUserWithChecks(Long id) throws BusinessException {
		assert id != null : "Parameter 'id' can not be null";

		// Load user.
		User user = userDao.loadById(id, false);

		// Check user.
		if (user == null) {
			log.warn("User with given id does not exist: [userId = '{}']", id);
			throw new BusinessException("User with given id does not exist: [userId = '" + id + "']");
		}

		return user;
	}

	@Override
	@Transactional(rollbackFor = BusinessException.class)
	public List<DataObjectDto> listDataObjects(Long userId) throws BusinessException {
		assert userId != null : "Parameter 'userId' can not be null";

		// Load user.
		User user = getUserWithChecks(userId);

		List<DataObjectDto> dataObjectDtoList = new ArrayList<DataObjectDto>();

		// Iterate over profiles.
		for (UserProfile userProfile : user.getUserProfiles()) {
			// Iterate over sources.
			for (DataSource dataSource : userProfile.getDataSources()) {
				// Load objects.
				List<DataObject> dataObjectList = feedDao.loadDataEntries(dataSource);

				log.trace("listDataObjects(): loaded data from data source size: [dataSource.name = '{}', dataObjectList.size = '{}']",
						dataSource.getName(), dataObjectList.size());

				// Go through loaded objects.
				for (DataObject dataObject : dataObjectList) {
					// Filter by profile tags.
					if (userProfile.getTags().size() > 0) {
						if (filterDataObjectUsingTags(dataObject, userProfile.getTags())) {
							// Convert model to dto.
							dataObjectDtoList.add(convertDataObjectModelToDto(dataObject, userProfile, dataSource));
						}
					}
					// Or load all.
					else {
						dataObjectDtoList.add(convertDataObjectModelToDto(dataObject, userProfile, dataSource));
					}
				}
			}
		}

		log.trace("listDataObjects(): final size of loaded and filtered data: [dataObjectDtoList.size = '{}']", dataObjectDtoList
				.size());

		return dataObjectDtoList;
	}

	@Override
	@Transactional(rollbackFor = BusinessException.class)
	public List<UserProfileDto> listUserProfiles(Long userId) throws BusinessException {
		assert userId != null : "Parameter 'userId' can not be null";

		// Load user.
		User user = getUserWithChecks(userId);

		// Load user profiles.
		List<UserProfile> userProfileList = userProfileDao.loadByUser(user, OrderBy.asc("name"));

		List<UserProfileDto> userProfileDtoList = new ArrayList<UserProfileDto>();
		// Convert user profile model to DTO list.
		for (UserProfile userProfile : userProfileList) {
			userProfileDtoList.add(ModelToDtoConverterUtils.convertUserProfileModelToDto(userProfile));
		}

		return userProfileDtoList;
	}

	@Override
	@Transactional(rollbackFor = BusinessException.class)
	public Long saveUserProfile(Long userId, UserProfileDto userProfileDto) throws BusinessException {
		assert userId != null : "Parameter 'userId' can not be null";
		assert userProfileDto != null : "Parameter 'userProfileDto' can not be null";

		// Load user.
		User user = getUserWithChecks(userId);

		UserProfile userProfile = null;
		// Update user profile.
		if (userProfileDto.getId() != null) {
			userProfile = userProfileDao.loadById(userProfileDto.getId(), true);
		}
		// Create new instance.
		else {
			userProfile = new UserProfile();
			userProfile.setUser(user);

			user.getUserProfiles().add(userProfile);
		}

		updateUserProfileModelAgainstDto(userProfile, userProfileDto);

		userProfileDao.save(userProfile);

		return userProfile.getId();
	}

	private DataObjectDto convertDataObjectModelToDto(DataObject dataObject, UserProfile userProfile, DataSource dataSource) {
		DataObjectDto dataObjectDto = new DataObjectDto();

		dataObjectDto.setUserProfileName(userProfile.getName());
		dataObjectDto.setDataSourceName(dataSource.getName());

		for (Entry<String, DataField> entry : dataObject.getDataFields().entrySet()) {
			DataField dataField = entry.getValue();
			dataObjectDto.getDataValues().put(entry.getKey(), dataField != null ? dataField.getValue() : null);
		}

		return dataObjectDto;
	}

	private boolean filterDataObjectUsingTags(DataObject dataObject, Set<Tag> tags) {
		// For all fields.
		for (DataField dataField : dataObject.getDataFields().values()) {
			// If field is set.
			if (dataField != null && dataField.getValue() != null) {
				String value = dataField.getValue();
				// Check field value against tags.
				for (Tag tag : tags) {
					if (value.toLowerCase().contains(tag.getValue().toLowerCase())) {
						return true;
					}
				}
			}
		}
		return false;
	}

	private void updateUserProfileModelAgainstDto(UserProfile model, UserProfileDto dto) throws BusinessException {
		BeanUtils.copyProperties(dto, model, new String[] { "id", "dataSources", "tags" });

		// Update data sources.
		model.getDataSources().clear();
		for (DataSourceDto dataSourceDto : dto.getDataSources()) {
			model.getDataSources().add(dataSourceService.getDataSourceWithChecks(dataSourceDto.getId()));
		}
		// Update tags
		model.getTags().clear();
		for (TagDto tagDto : dto.getTags()) {
			model.getTags().add(tagService.getTagWithChecks(tagDto.getId()));
		}
	}
}
